Text Mode Cursor

From OSDev Wiki
Jump to navigation Jump to search

In text mode, the cursor does not work the same way as in high-level languages, automatically moving to one place after the last written character. Instead, it is simply a blinking area that can be resized, shown, hidden, and moved by the OS.

With the BIOS

To manipulate the cursor with the BIOS, use int 0x10, the interrupt for screen functions.

Enabling the Cursor

Enabling the cursor also allows you to set the start and end scanlines, the rows where the cursor starts and ends. The highest scanline is 0 and the lowest scanline is the maximum scanline (usually 15).

  • AH = 0x01
  • CH = start scanline
  • CL = end scanline

Disabling the Cursor

  • AH = 0x01
  • CH = 0x3F (bits 6-7 unused, bit 5 disables cursor, bits 0-4 control cursor shape)

Moving the Cursor

  • AH = 0x02
  • BH = display page (usually, if not always 0)
  • DH = row
  • DL = column

Get Cursor Data

  • AH = 0x03
  • BH = display page (usually, if not always 0)

The return values:

  • CH = start scanline
  • CL = end scanline
  • DH = row
  • DL = column

Without the BIOS

Without BIOS access, manipulating the cursor requires sending data directly to the hardware.

Enabling the Cursor

Enabling the cursor also allows you to set the start and end scanlines, the rows where the cursor starts and ends. The highest scanline is 0 and the lowest scanline is the maximum scanline (usually 15).

Source in C

void enable_cursor(uint8_t cursor_start, uint8_t cursor_end)
{
	outb(0x3D4, 0x0A);
	outb(0x3D5, (inb(0x3D5) & 0xC0) | cursor_start);

	outb(0x3D4, 0x0B);
	outb(0x3D5, (inb(0x3D5) & 0xE0) | cursor_end);
}

Disabling the Cursor

Source in C

void disable_cursor()
{
	outb(0x3D4, 0x0A);
	outb(0x3D5, 0x20);
}

Source in Assembly

disable_cursor:
	pushf
	push eax
	push edx

	mov dx, 0x3D4
	mov al, 0xA	; low cursor shape register
	out dx, al

	inc dx
	mov al, 0x20	; bits 6-7 unused, bit 5 disables the cursor, bits 0-4 control the cursor shape
	out dx, al

	pop edx
	pop eax
	popf
	ret

Moving the Cursor

Keep in mind that you don't need to update the cursor's location every time a new character is displayed. It would be faster to instead only update it after printing an entire string.

Source in C

void update_cursor(int x, int y)
{
	uint16_t pos = y * VGA_WIDTH + x;

	outb(0x3D4, 0x0F);
	outb(0x3D5, (uint8_t) (pos & 0xFF));
	outb(0x3D4, 0x0E);
	outb(0x3D5, (uint8_t) ((pos >> 8) & 0xFF));
}

Source in Assembly

Cursor:
VGA.Width equ 80

.SetCoords:
; input bx = x, ax = y
; modifies ax, bx, dx

	mov dl, VGA.Width
	mul dl
	add bx, ax

.SetOffset:
; input bx = cursor offset
; modifies al, dx

	mov dx, 0x03D4
	mov al, 0x0F
	out dx, al

	inc dl
	mov al, bl
	out dx, al

	dec dl
	mov al, 0x0E
	out dx, al

	inc dl
	mov al, bh
	out dx, al
	ret

Get Cursor Position

With this code, you get: pos = y * VGA_WIDTH + x. To obtain the coordinates, just calculate: y = pos / VGA_WIDTH; x = pos % VGA_WIDTH;.

Source in C

uint16_t get_cursor_position(void)
{
    uint16_t pos = 0;
    outb(0x3D4, 0x0F);
    pos |= inb(0x3D5);
    outb(0x3D4, 0x0E);
    pos |= ((uint16_t)inb(0x3D5)) << 8;
    return pos;
}

Font based "graphical" cursor

Used by

Back in the DOS days it was quite common not to use the hardware VGA cursor at all, instead overriding the VGA fonts to create an arrow pointer just like in graphical modes. This technique was used in Norton Utilities or the DOS version of Norton Diskedit for example.

Arrow cursor in text mode

You can easily spot this font altering cursor by the cursor color: as only the fonts are altered and the attribute bytes are untouched, the pointer changes color as you move it around.

Here's another example from the Screenshots forum TUI with 8x8 characters and pointing finger-shaped cursor

How to implement

The basic principle is to store 4 bytes (2x2) from the screen, copy their VGA Fonts to some unused box drawing characters (0xC0 - 0xDF), OR mask the arrow onto then write those 2x2 box drawing characters on screen. Then when the mouse moved, the original 4 bytes are restored on screen, and the whole procedure repeated on the new position.

It is important to use box drawing characters, because normally VGA displays fonts as 9x16, adding an empty 9th column, which would cause a "gap" in the pointer. With the box drawing characters that 9th column is a copy of the 8th column, and therefore does not cut the pointer in half. If you use 8x8 characters (like 80x50 or 132x50 modes) then there are no character separator columns, and you are free to use any character you like. On the forum example above you can spot the cursor on the ASCII table at characters 0xF0 - 0xF3.

Although the arrow size is the same as one character (typically 8x16 or 8x8), as the pointer can be moved with pixel precision it can overlap with the next character both horizontally and vertically, thus giving the total 4 bytes requirement:

char 1   attr 1   char 2   attr 2 
........|????????|........|????????       first line
........|????????|........|????????       (note attribute bytes are untouched)
........|????????|........|????????
....x...|????????|........|????????
....xx..|????????|........|????????
....xxx.|????????|........|????????
....xxxx|????????|........|????????
....xxxx|????????|x.......|????????
--------+--------+--------+--------
....xxxx|????????|xx......|????????       second line
....xxxx|????????|xxx.....|????????
....xxxx|????????|xxxx....|????????
.......x|????????|x.......|????????
.......x|????????|x.......|????????
........|????????|xx......|????????
........|????????|........|????????
........|????????|........|????????
char 3   attr 3   char 4   attr 4

You can read the VGA Fonts with BIOS, or if you're already in protected mode, with VGA registers. Read VGA Fonts article for more information.

The character positions are calculated by dividing mouse coordinates by font size: cx = mx / 8 and cy = my / 16. Then you calculate my % 16 to get the first byte of the font glyph to be modified, and mx % 8 to get the shift value by which you have to shift the arrow mask.

A Note on GRUB

If the timeout is set to 0 in your grub.cfg, the cursor will be disabled and you will need to enable it yourself. Otherwise, GRUB will enable the cursor for you. Because of this inconsistency, it is a good idea to always enable the cursor. Even if you don't set the timeout to 0, you might want to in the future, or someone might change it on their system.

See Also

External Links